package com.example.socket.codec.protobuf;/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.baidu.bjf.remoting.protobuf.EnumReadable;
import com.baidu.bjf.remoting.protobuf.FieldType;
import com.baidu.bjf.remoting.protobuf.ProtobufIDLGenerator;
import com.baidu.bjf.remoting.protobuf.annotation.Ignore;
import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;
import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
import com.baidu.bjf.remoting.protobuf.utils.FieldInfo;
import com.baidu.bjf.remoting.protobuf.utils.ProtobufProxyUtils;
import jodd.io.findfile.ClassScanner;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 *
 * Utility class for generate protobuf IDL content from @{@link Protobuf}
 *
 * @author frank
 * @since 1.0.1
 */
public class ImportProtobufIDLGenerator {


    /** Logger for this class. */
    private static final Logger LOGGER = LoggerFactory.getLogger(ProtobufIDLGenerator.class.getName());

    /** The Constant V3_HEADER. */
    private static final String V3_HEADER = "syntax = \"proto3\"";

    /**
     * get IDL content from class.
     *
     * @param cls target class to parse for IDL message.
     * @param cachedTypes if type already in set will not generate IDL. if a new type found will add to set
     * @param cachedEnumTypes if enum already in set will not generate IDL. if a new enum found will add to set
     * @param ignoreJava set true to ignore generate package and class name
     * @return protobuf IDL content in string
     * @see Protobuf
     */
    public static void getIDL(final Class<?> cls, final Set<Class<?>> cachedTypes,
                              final Set<Class<?>> cachedEnumTypes, Map<Class, ProtoFile> protoFiles, boolean ignoreJava) {
        Ignore ignore = cls.getAnnotation(Ignore.class);
        IgnoreProto ignoreProto = cls.getAnnotation(IgnoreProto.class);
        if (ignore != null || ignoreProto != null) {
            LOGGER.info("class '{}' marked as @Ignore annotation, create IDL ignored.", cls.getName());
            return;
        }

        Set<Class<?>> types = cachedTypes;
        if (types == null) {
            types = new HashSet<>();
        }

        Set<Class<?>> enumTypes = cachedEnumTypes;
        if (enumTypes == null) {
            enumTypes = new HashSet<>();
        }

        if (types.contains(cls)) {
            return;
        }
        // define outer name class
        types.add(cls);

        ProtoFile protoFile = getProtoFile(cls, protoFiles, ignoreJava);

        generateIDL(protoFile, protoFiles, cls, types, enumTypes, ignoreJava);
    }


    private static ProtoFile getProtoFile(Class<?> cls, Map<Class, ProtoFile> protoFiles, boolean ignoreJava) {
        ProtoFile protoFile = protoFiles.get(cls);
        if (protoFile != null) {
            return protoFile;
        }
        protoFile = ProtoFile.valueOf(cls);
        StringBuilder header = protoFile.getHeader();
        header.append(V3_HEADER).append(";\n");
        if (!ignoreJava) {
            // define package
            header.append("package ").append(cls.getPackage().getName()).append(";\n");
            header.append("option java_outer_classname = \"").append(cls.getSimpleName()).append("$$ByJProtobuf\";\n");
        }

        protoFiles.put(cls, protoFile);
        return protoFile;
    }

    /**
     */
    private static void generateIDL(ProtoFile protoFile, Map<Class, ProtoFile> protoFiles, Class<?> cls, Set<Class<?>> cachedTypes,
                                    Set<Class<?>> cachedEnumTypes, boolean ignoreJava) {
        StringBuilder code = protoFile.getBody();
        Set<Class<?>> subTypes = new HashSet<Class<?>>();
        Set<Class<Enum>> enumTypes = new HashSet<Class<Enum>>();
        code.append("message ").append(cls.getSimpleName()).append(" {  \n");

        List<FieldInfo> fieldInfos = ProtobufProxyUtils.fetchFieldInfos(cls, false);
        boolean isMap = false;
        for (FieldInfo field : fieldInfos) {
            if (field.getField().getName().equals("Companion")) {
                continue;
            }
            Class c = null;
            if (field.hasDescription()) {
                code.append("\t").append("// ").append(field.getDescription()).append("\n");
            }
            if (field.getFieldType() == FieldType.OBJECT || field.getFieldType() == FieldType.ENUM) {
                if (field.isList()) {
                    Type type = field.getField().getGenericType();
                    if (type instanceof ParameterizedType) {
                        ParameterizedType ptype = (ParameterizedType) type;

                        Type[] actualTypeArguments = ptype.getActualTypeArguments();
                        if (actualTypeArguments != null && actualTypeArguments.length > 0) {
                            Type targetType = actualTypeArguments[0];
                            if (targetType instanceof Class || targetType instanceof WildcardType) {

                                if (targetType instanceof Class) {
                                    c = (Class) targetType;
                                } else {
                                    c = (Class) ((WildcardType) targetType).getUpperBounds()[0];
                                }

                                String fieldTypeName;
                                if (ProtobufProxyUtils.isScalarType(c)) {

                                    FieldType fieldType = ProtobufProxyUtils.TYPE_MAPPING.get(c);
                                    fieldTypeName = fieldType.getType();

                                } else {
                                    if (field.getFieldType() == FieldType.ENUM) {
                                        if (!cachedEnumTypes.contains(c)) {
                                            cachedEnumTypes.add(c);
                                            enumTypes.add(c);
                                        }
                                    } else {

                                        if (!cachedTypes.contains(c)) {
                                            cachedTypes.add(c);
                                            subTypes.add(c);
                                        }
                                    }

                                    fieldTypeName = c.getSimpleName();
                                    if (isDependency(cls, c) && !ignoreJava) {
                                        fieldTypeName = c.getName();
                                    }
                                }

                                code.append("\t").append("repeated ").append(fieldTypeName).append(" ")
                                        .append(field.getField().getName()).append(" = ").append(field.getOrder())
                                        .append(";\n");
                            }
                        }
                    }
                } else {
                    c = field.getField().getType();
                    String fieldTypeName = c.getSimpleName();
                    if (isDependency(cls, c) && !ignoreJava) {
                        fieldTypeName = c.getName();
                    }
                    code.append("\t").append(getFieldRequired(field.isRequired())).append(fieldTypeName).append(" ")
                            .append(field.getField().getName()).append(" = ").append(field.getOrder()).append(";\n");
                    if (field.getFieldType() == FieldType.ENUM) {
                        if (!cachedEnumTypes.contains(c)) {
                            cachedEnumTypes.add(c);
                            enumTypes.add(c);
                        }
                    } else {

                        if (!cachedTypes.contains(c)) {
                            cachedTypes.add(c);
                            subTypes.add(c);
                        }
                    }
                }
            } else {
                String type = field.getFieldType().getType().toLowerCase();

                if (field.getFieldType() == FieldType.ENUM) {
                    // if enum type
                    c = field.getField().getType();
                    if (Enum.class.isAssignableFrom(c)) {
                        type = c.getSimpleName();
                        if (isDependency(cls, c) && !ignoreJava) {
                            type = c.getName();
                        }
                        if (!cachedEnumTypes.contains(c)) {
                            cachedEnumTypes.add(c);
                            enumTypes.add(c);
                        }
                    }
                } else if (field.getFieldType() == FieldType.MAP) {
                    isMap = true;
                    Class keyClass = field.getGenericKeyType();
                    Class valueClass = field.getGenericeValueType();
                    if (isDependency(cls, keyClass) && !ignoreJava) {
                        protoFile.addImport(keyClass);
                        type = type + "<" + keyClass.getName() + ", ";
                    } else {
                        type = type + "<" + ProtobufProxyUtils.processProtobufType(keyClass) + ", ";
                    }
                    if (isDependency(cls, valueClass) && !ignoreJava) {
                        protoFile.addImport(valueClass);
                        type = type + valueClass.getName() + ">";
                    } else {
                        type = type + ProtobufProxyUtils.processProtobufType(valueClass) + ">";
                    }

                    // check map key or value is object type
                    if (ProtobufProxyUtils.isObjectType(keyClass)) {
                        if (Enum.class.isAssignableFrom(keyClass)) {
                            if (!cachedEnumTypes.contains(keyClass)) {
                                enumTypes.add(keyClass);
                            }
                        } else {
                            if (!cachedTypes.contains(keyClass)) {
                                subTypes.add(keyClass);
                            }
                        }
                    }

                    if (ProtobufProxyUtils.isObjectType(valueClass)) {
                        if (Enum.class.isAssignableFrom(valueClass)) {
                            if (!cachedEnumTypes.contains(valueClass)) {
                                enumTypes.add(valueClass);
                            }
                        } else {
                            if (!cachedTypes.contains(valueClass)) {
                                subTypes.add(valueClass);
                            }
                        }
                    }

                }

                String required = getFieldRequired(field.isRequired());
                if (isMap) {
                    required = "";
                }

                if (field.isList()) {
                    required = "repeated ";
                }

                code.append("\t").append(required).append(type).append(" ").append(field.getField().getName())
                        .append(" = ").append(field.getOrder()).append(";\n");
            }
            //添加依赖
            if (isDependency(cls, c)) {
                protoFile.addImport(c);
            }
        }

        code.append("}\n\n");

        for (Class<Enum> subType : enumTypes) {
            ProtoFile subProtoFile = getProtoFile(subType, protoFiles, ignoreJava);
            generateEnumIDL(subProtoFile, subType, ignoreJava);
        }

        if (subTypes.isEmpty()) {
            return;
        }

        for (Class<?> subType : subTypes) {
            ProtoFile subProtoFile = getProtoFile(subType, protoFiles, ignoreJava);
            generateIDL(subProtoFile, protoFiles, subType, cachedTypes, cachedEnumTypes, ignoreJava);
        }
    }

    private static boolean isDependency(Class cls, Class field) {
        return field != null && !ProtobufProxyUtils.isScalarType(field) && !field.equals(cls);
    }

    private static void generateEnumIDL(ProtoFile protoFile, Class<Enum> cls, boolean ignoreJava) {
        StringBuilder code = protoFile.getBody();
        code.append("enum ").append(cls.getSimpleName()).append(" {  \n");

        Field[] fields = cls.getFields();
        for (Field field : fields) {

            String name = field.getName();
            Class c = field.getDeclaringClass();
            if (isDependency(cls, c) && !ignoreJava) {
                name = c.getName();
            }
            code.append("\t").append(name).append(" = ");
            try {
                Enum value = Enum.valueOf(cls, name);
                if (value instanceof EnumReadable) {
                    code.append(((EnumReadable) value).value());
                } else {
                    code.append(value.ordinal());
                }
                code.append(";\n");
            } catch (Exception e) {
                continue;
            }
        }

        code.append("}\n ");
    }

    /**
     * @param required
     * @return
     */
    private static String getFieldRequired(boolean required) {
        return "";
    }

    private static void generatorProtos(String basePackages, boolean ignoreJava,String path) throws IOException, ClassNotFoundException {
        final Set<Class<?>> cachedTypes = new HashSet<>();
        final Set<Class<?>> cachedEnumTypes = new HashSet<>();
        final Map<Class, ProtoFile> protoFiles = new HashMap<>();
        ClassScanner scanner = new ClassScanner() {
            @Override
            protected void onEntry(EntryData entryData) {
                String name = entryData.getName();
                if (!name.startsWith(basePackages)) {
                    return;
                }
                Class c = toClass(name);
                if (c == null) {
                    return;
                }
                if (Enum.class.isAssignableFrom(c)) {
                    return;
                }
                Annotation annotation = c.getAnnotation(ProtobufClass.class);
                if (annotation != null) {
                    getIDL(c, cachedTypes, cachedEnumTypes, protoFiles, ignoreJava);
                }
            }
        };
        scanner.scanDefaultClasspath();
        if (path == null) {
            path= ImportProtobufIDLGenerator.class.getClassLoader().getResource("").getPath();
        }
        for (ProtoFile protoFile : protoFiles.values()) {
            protoFile.flush(path);
        }

    }

    private static Class toClass(String name) {
        try {
            return Thread.currentThread().getContextClassLoader().loadClass(name);
        } catch (Throwable e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(e.getMessage(), e);
            }
        }
        return null;
    }


    /**
     * @author frank
     */
    private static class ProtoFile {
        private String pkg;
        private Class cls;
        private StringBuilder header = new StringBuilder();
        private Set<Class> imports = new HashSet<>();
        private StringBuilder body = new StringBuilder();

        public static ProtoFile valueOf(Class cls) {
            ProtoFile result = new ProtoFile();
            result.cls = cls;
            result.pkg = cls.getPackage().getName();
            return result;
        }

        public String getPkg() {
            return pkg;
        }

        public void setPkg(String pkg) {
            this.pkg = pkg;
        }

        public Class getCls() {
            return cls;
        }

        public void setCls(Class cls) {
            this.cls = cls;
        }

        public Set<Class> getImports() {
            return imports;
        }

        public void setImports(Set<Class> imports) {
            this.imports = imports;
        }

        public void addImport(Class cls) {
            this.imports.add(cls);
        }

        public StringBuilder getHeader() {
            return header;
        }

        public void setHeader(StringBuilder header) {
            this.header = header;
        }

        public StringBuilder getBody() {
            return body;
        }

        public void setBody(StringBuilder body) {
            this.body = body;
        }

        public String toSchema() {
            for (Class cls : imports) {
                String file = getFile(cls);
                header.append("import public \"" + file + "\";\n");
            }
            header.append("\n");
            header.append(body);
            return header.toString();
        }

        public String getFile(Class cls) {
            return cls.getPackage().getName().replace(".", File.separator) + File.separator + cls.getSimpleName() + ".proto";
        }

        public String getFile() {
            return pkg.replace(".", File.separator) + File.separator + this.cls.getSimpleName() + ".proto";
        }

        public void flush(String output) {
            String dir = output + File.separator + pkg.replace('.', File.separatorChar);
            File f = new File(dir);
            f.mkdirs();
            String fileName = cls.getSimpleName() + ".proto";
            File file = new File(dir, fileName);
            LOGGER.info("Generate proto file to " + file.getAbsolutePath());
            try {
                FileUtils.writeByteArrayToFile(file, toSchema().getBytes(StandardCharsets.UTF_8));
            } catch (IOException e) {
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
        }
    }


    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        String outPath = "/Users/frank/Documents/work/java/game/game-protoc/pojo/target/generated/protos";
        generatorProtos("com.game.pojo", false, null);
    }


}
